package com.googlecode.curvedmetromaps.core.presentation;

import java.util.ArrayList;

import org.jbox2d.common.Vec2;

import playn.core.*;
import playn.core.Font.Style;

import com.googlecode.curvedmetromaps.core.logic.costfunction.InflectionCost;
import com.googlecode.curvedmetromaps.core.model.MetroEdge;
import com.googlecode.curvedmetromaps.core.model.MetroLine;
import com.googlecode.curvedmetromaps.core.model.MetroMap;
import com.googlecode.curvedmetromaps.core.model.MetroStation;

public class Renderer {
	public final int	stationColor	= 0xFF000000;
	public final int	edgeColor		= 0xFF000000;
	public final float	stationRadius	= 5;
	public final float	padding			= 0.02f;

	// Mouse debug position
	public float		mouseX			= 0;
	public float		mouseY			= 0;

	/** Variables to scale model to canvas coordinates */
	private float		modelXMin, modelYMax, modelScale;

	/**
	 * Draws metro map on the canvas.
	 * @param canvas PlayN canvas to draw upon.
	 * @param map Metro map to draw on canvas.
	 */
	public void drawMetroMap(Canvas canvas, Graphics graphics, MetroMap map) {
		updateModelScaling(canvas, map);

		for (int i = 0; i < map.lines.length; i++) {
			MetroLine metroLine = map.lines[i];
			drawMetroLine(canvas, map, metroLine);
		}
		
		InflectionCost infCost = new InflectionCost(map);
		infCost.calculateCost();

		for (MetroStation station : map.stations) {
			drawMetroStation(canvas, graphics, station, infCost.inflectionPoints);
		}

		// drawGeoForces(canvas, map);
		// drawEdgeRepulsiveForces(canvas, map);
		// drawMouseEdgeProjections(canvas, map);
	}

	private void drawGeoForces(Canvas canvas, MetroMap map) {
		for (MetroStation m1 : map.stations) {
			for (MetroStation m2 : map.stations) {
				float force = map.geoForces[m1.id][m2.id];
				if (m1.id < m2.id && force > 0) {
					float forceFactor = Math.min(1.0f, force / 500f);
					int color = 0x00FF0000 + 0x01000000 * (int) (forceFactor * 0xFF);
					canvas.setStrokeColor(color);
					canvas.setStrokeWidth(1f);
					canvas.drawLine(getCanvasX(m1.x), getCanvasY(m1.y), getCanvasX(m2.x), getCanvasY(m2.y));
				}
			}
		}
	}

	private void drawEdgeRepulsiveForces(Canvas canvas, MetroMap map) {
		for (MetroStation m : map.stations) {
			for (Vec2 v : m.edgeProj) {
				// float forceFactor = Math.min(1.0f, 100f / v.length());
				float forceFactor = 1f;
				int color = 0x00FF0000 + 0x01000000 * (int) (forceFactor * 0xFF);
				canvas.setStrokeColor(color);
				canvas.setStrokeWidth(1f);
				canvas.drawLine(getCanvasX(m.x), getCanvasY(m.y), getCanvasX(v.x), getCanvasY(v.y));
			}
		}
	}

	private void drawMouseEdgeProjections(Canvas canvas, MetroMap map) {
		canvas.setFillColor(0xFFFF0000);
		canvas.fillCircle(mouseX, mouseY, 4f);

		for (MetroLine line : map.lines) {
			for (int i = 1; i < line.stations.length; i++) {
				drawMetroLine(canvas, map, line);
				MetroStation m1 = line.stations[i - 1];
				MetroStation m2 = line.stations[i];
				MetroEdge e = map.getEdge(m1, m2);
				if (e != null) {
					Vec2 mousePoint = new Vec2(mouseX, mouseY);

					MetroStation m1Canvas = new MetroStation();
					MetroStation m2Canvas = new MetroStation();
					m1Canvas.x = getCanvasX(m1.x);
					m1Canvas.y = getCanvasY(m1.y);
					m2Canvas.x = getCanvasX(m2.x);
					m2Canvas.y = getCanvasY(m2.y);

					MetroEdge eCanvas = new MetroEdge(m1Canvas, m2Canvas);
					eCanvas.cpX = getCanvasX(e.cpX);
					eCanvas.cpY = getCanvasY(e.cpY);

					Vec2 projection = eCanvas.pointProjection(mousePoint);
					float len = Math.min(1f, Math.max(0f, 1f - (projection.sub(mousePoint).length() / 100f)));
					int color = 0x00FF0000 + 0x01000000 * (int) (len * 0xFF);
					canvas.setStrokeColor(color);
					canvas.setStrokeWidth(1f);
					canvas.drawLine(projection.x, projection.y, mouseX, mouseY);
				}
			}
		}
	}

	private void updateModelScaling(Canvas canvas, MetroMap map) {
		// Calculate bounding box of model
		float modelXMax, modelYMin;
		modelXMin = Float.POSITIVE_INFINITY;
		modelXMax = Float.NEGATIVE_INFINITY;
		modelYMin = Float.POSITIVE_INFINITY;
		modelYMax = Float.NEGATIVE_INFINITY;
		for (MetroStation m : map.stations) {
			modelXMin = Math.min(modelXMin, m.x);
			modelXMax = Math.max(modelXMax, m.x);
			modelYMin = Math.min(modelYMin, m.y);
			modelYMax = Math.max(modelYMax, m.y);
		}

		// Add padding
		modelXMin -= padding * (modelXMax - modelXMin);
		modelXMax += padding * (modelXMax - modelXMin);
		modelYMin -= padding * (modelYMax - modelYMin);
		modelYMax += padding * (modelYMax - modelYMin);

		float modelWidth = modelXMax - modelXMin;
		float modelHeight = modelYMax - modelYMin;

		// Derive scale such that model fits on screen and aspect ratio is
		// maintained.
		float w = canvas.width();
		float h = canvas.height();
		float modelScaleW = w / modelWidth;
		float modelScaleH = h / modelHeight;
		modelScale = Math.min(modelScaleW, modelScaleH);
	}

	private float getCanvasX(float modelX) {
		return (modelX - modelXMin) * modelScale;
	}

	private float getCanvasY(float modelY) {
		return (modelYMax - modelY) * modelScale;
	}

	/**
	 * Draws metro line as a series of quadratic Bézier curves.
	 * @param canvas Canvas to draw upon.
	 * @param map MetroMap which must contain line.
	 * @param line MetroLine to draw on canvas.
	 */
	private void drawMetroLine(Canvas canvas, MetroMap map, MetroLine line) {
		Path path = canvas.createPath();
		for (int i = 1; i < line.stations.length; i++) {
			MetroStation m1 = line.stations[i - 1];
			MetroStation m2 = line.stations[i];
			MetroEdge e = map.getEdge(m1, m2);
			if (e != null) {
				// Draw connection
				if (i == 1)
					path.moveTo(getCanvasX(m1.x), getCanvasY(m1.y));

				// canvas.setFillColor(Colors.colors[line.id]);
				// canvas.fillRect(getCanvasX(m1.bsX), getCanvasY(m1.bsY), 3.0f,
				// 3.0f);
				// canvas.fillRect(getCanvasX(m2.bsX), getCanvasY(m2.bsY), 3.0f,
				// 3.0f);

				// Bézier curve
				canvas.setStrokeColor(0xFF000000);
				canvas.setStrokeWidth(3);
				// canvas.drawLine(getCanvasX(m1.x), getCanvasY(m1.y),
				// getCanvasX(e.cpX), getCanvasY(e.cpY));
				// canvas.drawLine(getCanvasX(e.cpX), getCanvasY(e.cpY),
				// getCanvasX(m2.x), getCanvasY(m2.y));

				path.quadraticCurveTo(getCanvasX(e.cpX), getCanvasY(e.cpY), getCanvasX(m2.x), getCanvasY(m2.y));
			}
		}
		
		int color = line.color != 0 ? line.color : Colors.colors[line.id];
		canvas.setStrokeColor(color);
		canvas.setStrokeWidth(5);
		canvas.strokePath(path);

		// Draw control points
		for (int i = 1; i < line.stations.length; i++) {
			MetroStation m1 = line.stations[i - 1];
			MetroStation m2 = line.stations[i];
			MetroEdge e = map.getEdge(m1, m2);
			if (e != null) {
//				drawControlPoint(canvas, e);
			}
		}
	}

	@SuppressWarnings("unused")
	private void drawControlPoint(Canvas canvas, MetroEdge e) {
		canvas.setStrokeColor(0xFF000000);
		canvas.setStrokeWidth(1f);
		canvas.drawPoint(getCanvasX(e.cpX), getCanvasY(e.cpY));
	}

	private void drawMetroStation(Canvas canvas, Graphics graphics, MetroStation m, ArrayList<Integer> inflectionPoints) {
		float canvasX = getCanvasX(m.x);
		float canvasY = getCanvasY(m.y);
		// Black circle
		canvas.setFillColor(0xFF000000);
		canvas.fillCircle(canvasX, canvasY, stationRadius);
		// White circle
		canvas.setFillColor(0xFFFFFFFF);
		// DEBUG
//		if (inflectionPoints.contains(m.id)) {
//			canvas.setFillColor(0xFFFF0000);
//		}
		canvas.fillCircle(canvasX, canvasY, stationRadius - 1);

		if (m.degree >= 1) {
			String label = m.label != null ? m.label : m.ref;
			// TEST
			//String label = m.ref + " " + m.label;
			
			TextFormat textFormat = new TextFormat();
			textFormat = textFormat.withFont(graphics.createFont("Arial", Style.PLAIN, 3f));
			TextLayout textLayout = graphics.layoutText(label, textFormat);
			canvas.setFillColor(0xFF000000);
		
			canvas.fillText(textLayout, canvasX + 2, canvasY + 2);
		}
	}
}
